/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2025 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.jscript;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.concurrent.ConcurrentHashMap;

import net.sf.acegisecurity.AuthenticationException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.ImporterTopLevel;
import org.mozilla.javascript.Script;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.WrapFactory;
import org.mozilla.javascript.WrappedException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.FileCopyUtils;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.processor.ProcessorExtension;
import org.alfresco.repo.processor.BaseProcessor;
import org.alfresco.scripts.ScriptException;
import org.alfresco.scripts.ScriptResourceHelper;
import org.alfresco.scripts.ScriptResourceLoader;
import org.alfresco.service.cmr.model.FileInfo;
import org.alfresco.service.cmr.model.FileNotFoundException;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.ScriptLocation;
import org.alfresco.service.cmr.repository.ScriptProcessor;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.namespace.QName;

/**
 * Implementation of the ScriptProcessor using the Rhino JavaScript library.
 * 
 * @author Kevin Roast
 */
public class RhinoScriptProcessor extends BaseProcessor implements ScriptProcessor, ScriptResourceLoader, InitializingBean
{
    private static final Log logger = LogFactory.getLog(RhinoScriptProcessor.class);
    private static final Log callLogger = LogFactory.getLog(RhinoScriptProcessor.class.getName() + ".calls");

    private static final String PATH_CLASSPATH = "classpath:";

    /** Wrap Factory */
    private static final WrapFactory wrapFactory = new RhinoWrapFactory();

    /** Sandbox Wrap Factory */
    private static final SandboxWrapFactory sandboxFactory = new SandboxWrapFactory();

    /** Base Value Converter */
    private final ValueConverter valueConverter = new ValueConverter();

    /** Store into which to resolve cm:name based script paths */
    private StoreRef storeRef;

    /** Store root path to resolve cm:name based scripts path from */
    private String storePath;

    /** Pre initialized secure scope object. */
    private Scriptable secureScope;

    /** Pre initialized non secure scope object. */
    private Scriptable nonSecureScope;

    /** Flag to enable or disable runtime script compliation */
    private boolean compile = true;

    /** Flag to enable the sharing of sealed root scopes between scripts executions */
    private boolean shareSealedScopes = true;

    /** Cache of runtime compiled script instances */
    private final Map<String, Script> scriptCache = new ConcurrentHashMap<String, Script>(256);

    /** Rhino optimization level */
    private int optimizationLevel = -1;

    /** Maximum seconds a script is allowed to run */
    private int maxScriptExecutionSeconds = -1;

    /** Maximum of call stack depth (in terms of number of call frames) */
    private int maxStackDepth = -1;

    /** Maximum memory (bytes) a script can use */
    private long maxMemoryUsedInBytes = -1L;

    /** Number of (bytecode) instructions that will trigger the observer */
    private int observerInstructionCount = 100;

    /** Flag to enable or disable scope cleaning at the end of each script execution */
    private boolean cleanScope = true;

    /** Custom context factory */
    public static AlfrescoContextFactory contextFactory;

    /**
     * Set the default store reference
     * 
     * @param storeRef
     *            The default store reference
     */
    public void setStoreUrl(String storeRef)
    {
        this.storeRef = new StoreRef(storeRef);
    }

    /**
     * @param storePath
     *            The store path to set.
     */
    public void setStorePath(String storePath)
    {
        this.storePath = storePath;
    }

    /**
     * @param compile
     *            the compile flag to set
     */
    public void setCompile(boolean compile)
    {
        this.compile = compile;
    }

    /**
     * @param shareSealedScopes
     *            true to allow sharing of sealed scopes between script executions - set to false to disable this feature and ensure that a new scope is created for each executed script.
     */
    public void setShareSealedScopes(boolean shareSealedScopes)
    {
        this.shareSealedScopes = shareSealedScopes;
    }

    /**
     * @param optimizationLevel
     *            -1 interpretive mode, 0 no optimizations, 1-9 optimizations performed
     */
    public void setOptimizationLevel(int optimizationLevel)
    {
        this.optimizationLevel = optimizationLevel;
    }

    /**
     * @param maxScriptExecutionSeconds
     *            the number of seconds a script is allowed to run
     */
    public void setMaxScriptExecutionSeconds(int maxScriptExecutionSeconds)
    {
        this.maxScriptExecutionSeconds = maxScriptExecutionSeconds;
    }

    /**
     * @param maxStackDepth
     *            the number of call stack depth allowed
     */
    public void setMaxStackDepth(int maxStackDepth)
    {
        this.maxStackDepth = maxStackDepth;
    }

    /**
     * @param maxMemoryUsedInBytes
     *            the number of memory a script can use
     */
    public void setMaxMemoryUsedInBytes(long maxMemoryUsedInBytes)
    {
        this.maxMemoryUsedInBytes = maxMemoryUsedInBytes;
    }

    /**
     * @param observerInstructionCount
     *            the number of instructions that will trigger {@link ContextFactory#observeInstructionCount}
     */
    public void setObserverInstructionCount(int observerInstructionCount)
    {
        this.observerInstructionCount = observerInstructionCount;
    }

    /**
     * @param cleanScope
     *            true to enable scope cleaning at the end of each script execution - set to false to disable this feature.
     */
    public void setCleanScope(boolean cleanScope)
    {
        this.cleanScope = cleanScope;
    }

    /**
     * @see org.alfresco.service.cmr.repository.ScriptProcessor#reset()
     */
    public void reset()
    {
        this.scriptCache.clear();
    }

    /**
     * @see org.alfresco.service.cmr.repository.ScriptProcessor#execute(org.alfresco.service.cmr.repository.ScriptLocation, java.util.Map)
     */
    public Object execute(ScriptLocation location, Map<String, Object> model)
    {
        try
        {
            // test the cache for a pre-compiled script matching our path
            Script script = null;
            String path = location.getPath();
            if (this.compile && location.isCachable())
            {
                script = this.scriptCache.get(path);
            }
            if (script == null)
            {
                if (logger.isDebugEnabled())
                    logger.debug("Resolving and compiling script path: " + path);

                // retrieve script content and resolve imports
                ByteArrayOutputStream os = new ByteArrayOutputStream();
                FileCopyUtils.copy(location.getInputStream(), os); // both streams are closed
                byte[] bytes = os.toByteArray();
                String source = new String(bytes, "UTF-8");
                source = resolveScriptImports(new String(bytes));

                // compile the script and cache the result
                Context cx = Context.enter();
                try
                {
                    script = cx.compileString(source, location.toString(), 1, null);

                    // We do not worry about more than one user thread compiling the same script.
                    // If more than one request thread compiles the same script and adds it to the
                    // cache that does not matter - the results will be the same. Therefore we
                    // rely on the ConcurrentHashMap impl to deal both with ensuring the safety of the
                    // underlying structure with asynchronous get/put operations and for fast
                    // multi-threaded access to the common cache.
                    if (this.compile && location.isCachable())
                    {
                        this.scriptCache.put(path, script);
                    }
                }
                finally
                {
                    Context.exit();
                }
            }

            String debugScriptName = null;
            if (callLogger.isDebugEnabled())
            {
                int i = path.lastIndexOf('/');
                debugScriptName = (i != -1) ? path.substring(i + 1) : path;
            }
            return executeScriptImpl(script, model, location.isSecure(), debugScriptName);
        }
        catch (Throwable err)
        {
            throw new ScriptException("Failed to execute script '" + location.toString() + "': " + err.getMessage(), err);
        }
    }

    /**
     * @see org.alfresco.service.cmr.repository.ScriptProcessor#execute(java.lang.String, java.util.Map)
     */
    public Object execute(String location, Map<String, Object> model)
    {
        return execute(new ClasspathScriptLocation(location), model);
    }

    /**
     * @see org.alfresco.service.cmr.repository.ScriptProcessor#execute(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.namespace.QName, java.util.Map)
     */
    public Object execute(NodeRef nodeRef, QName contentProp, Map<String, Object> model)
    {
        try
        {
            if (this.services.getNodeService().exists(nodeRef) == false)
            {
                throw new AlfrescoRuntimeException("Script Node does not exist: " + nodeRef);
            }

            if (contentProp == null)
            {
                contentProp = ContentModel.PROP_CONTENT;
            }
            ContentReader cr = this.services.getContentService().getReader(nodeRef, contentProp);
            if (cr == null || cr.exists() == false)
            {
                throw new AlfrescoRuntimeException("Script Node content not found: " + nodeRef);
            }

            // compile the script based on the node content
            Script script;
            Context cx = Context.enter();
            try
            {
                script = cx.compileString(resolveScriptImports(cr.getContentString()), nodeRef.toString(), 1, null);
            }
            finally
            {
                Context.exit();
            }

            return executeScriptImpl(script, model, false, nodeRef.toString());
        }
        catch (Throwable err)
        {
            throw new ScriptException("Failed to execute script '" + nodeRef.toString() + "': " + err.getMessage(), err);
        }
    }

    /**
     * @see org.alfresco.service.cmr.repository.ScriptProcessor#executeString(java.lang.String, java.util.Map)
     */
    public Object executeString(String source, Map<String, Object> model)
    {
        return executeString(source, model, false);
    }

    /**
     * @see org.alfresco.service.cmr.repository.ScriptProcessor#executeString(java.lang.String, java.util.Map, boolean)
     */
    public Object executeString(String source, Map<String, Object> model, boolean secure)
    {
        try
        {
            // compile the script based on the node content
            Script script;
            Context cx = Context.enter();
            try
            {
                script = cx.compileString(resolveScriptImports(source), "AlfrescoJS", 1, null);
            }
            finally
            {
                Context.exit();
            }
            return executeScriptImpl(script, model, secure, "string script");
        }
        catch (Throwable err)
        {
            throw new ScriptException("Failed to execute supplied script: " + err.getMessage(), err);
        }
    }

    /**
     * Resolve the imports in the specified script. Supported include directives are of the following form:
     * 
     * <pre>
     * &lt;import resource="classpath:alfresco/includeme.js"&gt;
     * &lt;import resource="workspace://SpacesStore/6f73de1b-d3b4-11db-80cb-112e6c2ea048"&gt;
     * &lt;import resource="/Company Home/Data Dictionary/Scripts/includeme.js"&gt;
     * </pre>
     * 
     * Either a classpath resource, NodeRef or cm:name path based script can be includes. Multiple includes of the same script are dealt with correctly and nested includes of scripts is fully supported.
     * <p>
     * Note that for performance reasons the script import directive syntax and placement in the file is very strict. The import lines <i>must</i> always be first in the file - even before any comments. Immediately that the script service detects a non-import line it will assume the rest of the file is executable script and no longer attempt to search for any further import directives. Therefore all imports should be at the top of the script, one following the other, in the correct syntax and with no comments present - the only separators valid between import directives is white space.
     * 
     * @param script
     *            The script content to resolve imports in
     * 
     * @return a valid script with all nested includes resolved into a single script instance
     */
    private String resolveScriptImports(String script)
    {
        return ScriptResourceHelper.resolveScriptImports(script, this, logger);
    }

    /**
     * Load a script content from the specific resource path.
     * 
     * @param resource
     *            Resources can be of the form:
     * 
     *            <pre>
     * classpath:alfresco/includeme.js
     * workspace://SpacesStore/6f73de1b-d3b4-11db-80cb-112e6c2ea048
     * /Company Home/Data Dictionary/Scripts/includeme.js
     *            </pre>
     * 
     * @return the content from the resource, null if not recognised format
     * 
     * @throws AlfrescoRuntimeException
     *             on any IO or ContentIO error
     */
    public String loadScriptResource(String resource)
    {
        String result = null;

        if (resource.startsWith(PATH_CLASSPATH))
        {
            try
            {
                // Load from classpath
                String scriptClasspath = resource.substring(PATH_CLASSPATH.length());
                URL scriptResource = getClass().getClassLoader().getResource(scriptClasspath);
                if (scriptResource == null && scriptClasspath.startsWith("/"))
                {
                    // The Eclipse classloader prefers alfresco/foo to /alfresco/foo, try that
                    scriptResource = getClass().getClassLoader().getResource(scriptClasspath.substring(1));
                }
                if (scriptResource == null)
                {
                    throw new AlfrescoRuntimeException("Unable to locate included script classpath resource: " + resource);
                }
                InputStream stream = scriptResource.openStream();
                if (stream == null)
                {
                    throw new AlfrescoRuntimeException("Unable to load included script classpath resource: " + resource);
                }
                ByteArrayOutputStream os = new ByteArrayOutputStream();
                FileCopyUtils.copy(stream, os); // both streams are closed
                byte[] bytes = os.toByteArray();
                // create the string from the byte[] using encoding if necessary
                result = new String(bytes, "UTF-8");
            }
            catch (IOException err)
            {
                throw new AlfrescoRuntimeException("Unable to load included script classpath resource: " + resource);
            }
        }
        else
        {
            NodeRef scriptRef;
            if (resource.startsWith("/"))
            {
                // resolve from default SpacesStore as cm:name based path
                // TODO: remove this once FFS correctly allows name path resolving from store root!
                NodeRef rootNodeRef = this.services.getNodeService().getRootNode(this.storeRef);
                List<NodeRef> nodes = this.services.getSearchService().selectNodes(
                        rootNodeRef, this.storePath, null, this.services.getNamespaceService(), false);
                if (nodes.size() == 0)
                {
                    throw new AlfrescoRuntimeException("Unable to find store path: " + this.storePath);
                }
                StringTokenizer tokenizer = new StringTokenizer(resource, "/");
                List<String> elements = new ArrayList<String>(6);
                if (tokenizer.hasMoreTokens())
                {
                    tokenizer.nextToken();
                }
                while (tokenizer.hasMoreTokens())
                {
                    elements.add(tokenizer.nextToken());
                }
                try
                {
                    FileInfo fileInfo = this.services.getFileFolderService().resolveNamePath(nodes.get(0), elements);
                    scriptRef = fileInfo.getNodeRef();
                }
                catch (FileNotFoundException err)
                {
                    throw new AlfrescoRuntimeException("Unable to load included script repository resource: " + resource);
                }
            }
            else
            {
                scriptRef = new NodeRef(resource);
            }

            // load from NodeRef default content property
            try
            {
                ContentReader cr = this.services.getContentService().getReader(scriptRef, ContentModel.PROP_CONTENT);
                if (cr == null || cr.exists() == false)
                {
                    throw new AlfrescoRuntimeException("Included Script Node content not found: " + resource);
                }
                result = cr.getContentString();
            }
            catch (ContentIOException err)
            {
                throw new AlfrescoRuntimeException("Unable to load included script repository resource: " + resource);
            }
        }

        return result;
    }

    /**
     * Execute the supplied script content. Adds the default data model and custom configured root objects into the root scope for access by the script.
     * 
     * @param script
     *            The script to execute.
     * @param model
     *            Data model containing objects to be added to the root scope.
     * @param secure
     *            True if the script is considered secure and may access java.* libs directly
     * @param debugScriptName
     *            To identify the script in debug messages.
     * 
     * @return result of the script execution, can be null.
     * 
     * @throws AlfrescoRuntimeException
     */
    private Object executeScriptImpl(Script script, Map<String, Object> model, boolean secure, String debugScriptName)
            throws AlfrescoRuntimeException
    {
        Scriptable scope = null;

        long startTime = 0;
        if (callLogger.isDebugEnabled())
        {
            callLogger.debug(debugScriptName + " Start");
            startTime = System.nanoTime();
        }

        // Convert the model
        model = convertToRhinoModel(model);

        Context cx = Context.enter();
        try
        {
            // Create a thread-specific scope from one of the shared scopes.
            // See http://www.mozilla.org/rhino/scopes.html
            cx.setWrapFactory(secure ? wrapFactory : sandboxFactory);

            // Enables or disables execution limits based on secure flag
            enableLimits(cx, secure);

            if (this.shareSealedScopes)
            {
                Scriptable sharedScope = secure ? this.nonSecureScope : this.secureScope;
                scope = cx.newObject(sharedScope);
                scope.setPrototype(sharedScope);
                scope.setParentScope(null);
            }
            else
            {
                scope = initScope(cx, secure, false);
            }

            // there's always a model, if only to hold the util objects
            if (model == null)
            {
                model = new HashMap<String, Object>();
            }

            // add the global scripts
            for (ProcessorExtension ex : this.processorExtensions.values())
            {
                model.put(ex.getExtensionName(), ex);
            }

            // insert supplied object model into root of the default scope
            for (String key : model.keySet())
            {
                try
                {
                    // set the root scope on appropriate objects
                    // this is used to allow native JS object creation etc.
                    Object obj = model.get(key);
                    if (obj instanceof Scopeable)
                    {
                        ((Scopeable) obj).setScope(scope);
                    }

                    // convert/wrap each object to JavaScript compatible
                    Object jsObject = Context.javaToJS(obj, scope);

                    // insert into the root scope ready for access by the script
                    ScriptableObject.putProperty(scope, key, jsObject);
                }
                catch (AuthenticationException e)
                {
                    // ok, log and don't add to the root scope
                    logger.info("Unable to add " + key + " to root scope: ", e);
                }
            }

            // execute the script and return the result
            Object result = script.exec(cx, scope);

            // extract java object result if wrapped by Rhino
            return valueConverter.convertValueForJava(result);
        }
        catch (WrappedException w)
        {
            if (callLogger.isDebugEnabled())
            {
                callLogger.debug(debugScriptName + " Exception", w);
            }
            Throwable err = w.getWrappedException();
            if (err instanceof RuntimeException)
            {
                throw (RuntimeException) err;
            }
            throw new AlfrescoRuntimeException(err.getMessage(), err);
        }
        catch (Throwable err)
        {
            if (callLogger.isDebugEnabled())
            {
                callLogger.debug(debugScriptName + " Exception", err);
            }
            throw new AlfrescoRuntimeException(err.getMessage(), err);
        }
        finally
        {
            if (!secure && cleanScope)
            {
                unsetScope(model, scope);
            }
            Context.exit();

            if (callLogger.isDebugEnabled())
            {
                long endTime = System.nanoTime();

                String logMessage = debugScriptName + " End " + (endTime - startTime) / 1000000 + " ms";

                if (cx instanceof AlfrescoScriptContext)
                {
                    AlfrescoScriptContext acx = (AlfrescoScriptContext) cx;
                    long usedMemory = acx.getUsedMemory();
                    if (usedMemory > 0)
                    {
                        logMessage += " - Used memory: " + usedMemory + " bytes";
                    }
                }

                callLogger.debug(logMessage);
            }
        }
    }

    /**
     * Converts the passed model into a Rhino model
     * 
     * @param model
     *            the model
     * 
     * @return Map the converted model
     */
    private Map<String, Object> convertToRhinoModel(Map<String, Object> model)
    {
        Map<String, Object> newModel = null;
        if (model != null)
        {
            newModel = new HashMap<String, Object>(model.size());
            for (Map.Entry<String, Object> entry : model.entrySet())
            {
                if (entry.getValue() instanceof NodeRef)
                {
                    newModel.put(entry.getKey(), new ScriptNode((NodeRef) entry.getValue(), this.services));
                }
                else
                {
                    newModel.put(entry.getKey(), entry.getValue());
                }
            }
        }
        else
        {
            newModel = new HashMap<String, Object>(1, 1.0f);
        }
        return newModel;
    }

    /**
     * Rhino script value wrapper
     */
    private static class RhinoWrapFactory extends WrapFactory
    {
        protected Scriptable wrapBasicJavaObject(Context cx, Scriptable scope, Object javaObject, Class<?> staticType)
        {
            return super.wrapAsJavaObject(cx, scope, javaObject, staticType);
        }

        /* (non-Javadoc)
         * 
         * @see org.mozilla.javascript.WrapFactory#wrapAsJavaObject(org.mozilla.javascript.Context, org.mozilla.javascript.Scriptable, java.lang.Object, java.lang.Class) */
        public Scriptable wrapAsJavaObject(Context cx, Scriptable scope, Object javaObject, Class<?> staticType)
        {
            if (javaObject instanceof Map && !(javaObject instanceof ScriptableHashMap))
            {
                return new NativeMap(scope, (Map) javaObject);
            }
            return wrapBasicJavaObject(cx, scope, javaObject, staticType);
        }
    }

    /**
     * A {@link WrapFactory} that ensures {@link org.mozilla.javascript.NativeJavaObject} instances are of the {@link SandboxNativeJavaObject} variety.
     */
    private static class SandboxWrapFactory extends RhinoWrapFactory
    {
        /* (non-Javadoc)
         * 
         * @see org.mozilla.javascript.WrapFactory#wrapAsJavaObject(org.mozilla.javascript.Context, org.mozilla.javascript.Scriptable, java.lang.Object, java.lang.Class) */
        @Override
        protected Scriptable wrapBasicJavaObject(Context cx, Scriptable scope, Object javaObject, Class<?> staticType)
        {
            return new SandboxNativeJavaObject(scope, javaObject, staticType);
        }

    }

    /**
     * Pre initializes two scope objects (one secure and one not) with the standard objects preinitialised. This saves on very expensive calls to reinitialize a new scope on every web script execution. See http://www.mozilla.org/rhino/scopes.html
     * 
     * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
     */
    public void afterPropertiesSet() throws Exception
    {
        // Initialize context factory
        initContextFactory();

        // Initialize the secure scope
        Context cx = Context.enter();
        try
        {
            cx.setWrapFactory(wrapFactory);
            this.secureScope = initScope(cx, false, true);
        }
        finally
        {
            Context.exit();
        }

        // Initialize the non-secure scope
        cx = Context.enter();
        try
        {
            cx.setWrapFactory(sandboxFactory);
            this.nonSecureScope = initScope(cx, true, true);
        }
        finally
        {
            Context.exit();
        }
    }

    /**
     * Initializes a scope for script execution. The easiest way to embed Rhino is just to create a new scope this way whenever you need one. However, initStandardObjects() is an expensive method to call and it allocates a fair amount of memory.
     * 
     * @param cx
     *            the thread execution context
     * @param secure
     *            Do we consider the script secure? When <code>false</code> this ensures the script may not access insecure java.* libraries or import any other classes for direct access - only the configured root host objects will be available to the script writer.
     * @param sealed
     *            Should the scope be sealed, making it immutable? This should be <code>true</code> if a scope is to be reused.
     * @return the scope object
     */
    protected Scriptable initScope(Context cx, boolean secure, boolean sealed)
    {
        Scriptable scope;
        if (secure)
        {
            // Initialise the non-secure scope
            // allow access to all libraries and objects, including the importer
            // @see http://www.mozilla.org/rhino/ScriptingJava.html
            scope = new ImporterTopLevel(cx, sealed);
        }
        else
        {
            // Initialise the secure scope
            // This sets up "scope" to have access to all the standard JavaScript classes, but does not create global objects for any top-level Java packages.
            // In addition, the "Packages," "JavaAdapter," and "JavaImporter" classes, and the "getClass" function, are not initialized.
            scope = cx.initSafeStandardObjects(null, sealed);
        }
        return scope;
    }

    /**
     * Clean supplied scope and unset it from any model instance where it has been injected before
     *
     * @param model
     *            Data model containing objects from where scope will be unset
     * @param scope
     *            The scope to clean
     */
    private void unsetScope(Map<String, Object> model, Scriptable scope)
    {
        if (scope != null)
        {
            Object[] ids = scope.getIds();
            if (ids != null)
            {
                for (Object id : ids)
                {
                    try
                    {
                        deleteProperty(scope, id.toString());
                    }
                    catch (Exception e)
                    {
                        logger.info("Unable to delete id: " + id, e);
                    }
                }
            }
        }

        if (model != null)
        {
            for (String key : model.keySet())
            {
                try
                {
                    deleteProperty(scope, key);

                    Object obj = model.get(key);
                    if (obj instanceof Scopeable)
                    {
                        ((Scopeable) obj).setScope(null);
                    }
                }
                catch (Exception e)
                {
                    logger.info("Unable to unset model object " + key + " : ", e);
                }
            }
        }
    }

    /**
     * Deletes a property from the supplied scope, if property is not removable, then is set to null
     *
     * @param scope
     *            the scope object from where property will be removed
     * @param name
     *            the property name to delete
     */
    private void deleteProperty(Scriptable scope, String name)
    {
        if (scope != null && name != null)
        {
            if (!ScriptableObject.deleteProperty(scope, name))
            {
                ScriptableObject.putProperty(scope, name, null);
            }
            scope.delete(name);
        }
    }

    /**
     * Initializes the context factory with limits configuration
     */
    private synchronized void initContextFactory()
    {
        if (contextFactory == null)
        {
            contextFactory = new AlfrescoContextFactory();
            contextFactory.setOptimizationLevel(optimizationLevel);

            if (maxScriptExecutionSeconds > 0)
            {
                contextFactory.setMaxScriptExecutionSeconds(maxScriptExecutionSeconds);
            }

            if (maxMemoryUsedInBytes > 0L)
            {
                contextFactory.setMaxMemoryUsedInBytes(maxMemoryUsedInBytes);
            }

            if (maxStackDepth > 0)
            {
                contextFactory.setMaxStackDepth(maxStackDepth);
            }

            if (maxScriptExecutionSeconds > 0 || maxMemoryUsedInBytes > 0L)
            {
                contextFactory.setObserveInstructionCount(observerInstructionCount);
            }

            ContextFactory.initGlobal(contextFactory);
        }
    }

    /**
     * If script is considered secure no limits will be applied, otherwise, the limits are enabled and the script can be interrupted in case a limit has been reached.
     *
     * @param cx
     *            the Rhino scope
     * @param secure
     *            true if script execution is considered secure (e.g, deployed at classpath level)
     */
    private void enableLimits(Context cx, boolean secure)
    {
        if (cx != null)
        {
            if (cx instanceof AlfrescoScriptContext)
            {
                ((AlfrescoScriptContext) cx).setLimitsEnabled(!secure);
            }
        }
    }
}
